﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

namespace Cheetah_Fast_Banking_App
{
    public class DataSourceSingleton
    {
        #region Variables
        private static DataSourceSingleton _sessionSingleton = new DataSourceSingleton();
        private Account _account = null;
        private List<Account> _accounts = new List<Account>();
        private bool _loggedIn = false;
        #endregion // Variables

        #region Constructor
        /// <summary>
        /// Private constructor to force singleton use through GetInstance()
        /// </summary>
        private DataSourceSingleton()
        {
            RefreshXmlData();
        }
        #endregion // Constructor

        #region Properties
        public string XmlFile
        {
            get { return Properties.Settings.Default.LastXmlPath; }
            private set
            {
                Properties.Settings.Default.LastXmlPath = value;
                Properties.Settings.Default.Save();
            }
        }

        public Account Account
        {
            get { return _account; }
            set
            {
                _account = value;
                if (_account == null)
                {
                    _loggedIn = false;
                }
            }
        }

        public bool LoggedIn
        {
            get { return _loggedIn; }
        }

        public List<Account> Accounts
        {
            get { return _accounts; }
        }
        #endregion // Properties

        #region Methods
        public static DataSourceSingleton GetInstance()
        {
            return _sessionSingleton;
        }

        /// <summary>
        /// Sets Datasource for CFBA
        /// </summary>
        /// <param name="filePath">Path of File to set</param>
        /// <returns>Bool indicating valid data</returns>
        public bool SetDataSource(string filePath)
        {
            bool validData = true;

            if (!File.Exists(filePath))
            {
                return false;
            }
            else
            {
                try
                {
                    //Confirm file conforms to CFBA Data schema
                    XmlReaderSettings settings = new XmlReaderSettings();
                    settings.Schemas.Add(null, "Data//CFBADataSchema.xsd");
                    settings.ValidationType = ValidationType.Schema;
                    //Load doc
                    XmlDocument doc = new XmlDocument();
                    doc.Load(filePath);
                    //set reader
                    XmlReader reader = XmlReader.Create(new StringReader(doc.InnerXml), settings);
                    while (reader.Read()) ;
                }
                catch
                {
                    return false;
                }
            }
            XmlFile = filePath;
            return validData;
        }

        /// <summary>
        /// Given a Login and Password, load account data
        /// </summary>
        /// <param name="accountName">User's account name</param>
        /// <param name="passwordHash">User's Password</param>
        /// <returns>The corresponding account if the credentials are valid, otherwise null.</returns>
        public Account ValidateCredentials(string accountName, string passwordHash)
        {
            _accounts.Clear();
            RefreshXmlData();
            Account[] accounts = (from a in _accounts
                                  where a.AccountName == accountName &&
                                  a.PasswordHash == passwordHash
                                  select a).ToArray();

            // Return the account if a match was found, otherwise return null
            return (accounts.Length > 0) ? accounts[0] : null;
        }

        public void RefreshXmlData()
        {
            _accounts.Clear();
            XElement XEl = XElement.Load(XmlFile);

            var accounts = XEl.Elements("account");
            foreach (var account in accounts)
            {
                // Create the account class and initialize the values
                Account newAccount = new Account()
                {
                    Guid = account.Attribute("id").Value,
                    AccountName = account.Attribute("name").Value,
                    PasswordHash = account.Attribute("password_hash").Value
                };
                // Get the transactions associated with the account.
                var transactions = from t in account.Descendants("transactions").Elements("transaction")
                                   select new
                                   {
                                       amount = Decimal.Parse(t.Attribute("amount").Value),
                                       date = DateTime.Parse(t.Attribute("date").Value),
                                       description = t.Attribute("description").Value,
                                       id = t.Attribute("id").Value
                                   };
                decimal previousBalance = 0.0m;
                foreach (var trans in transactions)
                {
                    Transaction newTrans = new Transaction(trans.description, trans.date, trans.amount, trans.id);
                    newTrans.SetCurrentBalance(previousBalance);
                    previousBalance += newTrans.CurrentBalance;
                    newAccount.Transactions.Add(newTrans);
                }

                // Add the new account object to the list
                _accounts.Add(newAccount);
            }
        }

        /// <summary>
        /// This method attempts to open a new account using the provided credentials.
        /// If an error is encountered creating the account, it is indicated in the returned string.
        /// If no error is encountered, an empty string is returned.
        /// </summary>
        /// <param name="accountName">Account name for the account being opened.</param>
        /// <param name="password">Password for the account being opened (in clear text).</param>
        /// <param name="confirmedPassword">Confirmation of the password
        /// for the account being opened (in clear text).</param>
        /// <returns>A string indicating the error, if one was encountered.</returns>
        public string OpenAccount(string accountName, string password, string confirmedPassword)
        {
            // Check that the passwords match
            if (string.IsNullOrEmpty(password) || password != confirmedPassword)
            {
                return "The passwords did not match.";
            }

            // Check that the account does not already exist
            var accounts = from a in _accounts
                           where a.AccountName == accountName
                           select a;
            if (accounts.Count() == 0)
            {
                // The account name does not exist--open the account
                // Create a new Guid for the account
                Guid guid = Guid.NewGuid();
                XDocument doc = XDocument.Load(XmlFile);
                XElement accountElement = new XElement("account",
                                                    new XAttribute("id", guid.ToString()),
                                                    new XAttribute("name", accountName),
                                                    new XAttribute("password_hash", password));
                // Create a blank transactions element
                XElement transactionsElement = new XElement("transactions");
                accountElement.Add(transactionsElement);
                doc.Descendants("accounts").Last().Add(accountElement);
                doc.Save(XmlFile);
                // No errors
                return "";
            }
            else
            {
                // Account name already exists--report error
                return "The specified account name already exists.";
            }
        }

        /// <summary>
        /// This method attempts to close an account using the provided account name.
        /// If an error is encountered closing the account, it is indicated in the returned string.
        /// If no error is encountered, an empty string is returned.
        /// </summary>
        /// <param name="accountName">Account name for the account being opened.</param>
        /// <returns>A string indicating the error, if one was encountered.</returns>
        public string CloseAccount(string accountName)
        {
            // Check that the account does exists
            XDocument doc = XDocument.Load(XmlFile);
            try
            {
                // If one account isn't returned, the error is returned in the catch
                XElement account = (from a in doc.Descendants("account")
                                    where a.Attribute("name").Value == accountName
                                    select a).Single();

                // If no exception was thrown, remove the returned account.
                account.Remove();
                doc.Save(XmlFile);
                // No errors
                return "";
            }
            catch (Exception)
            {
                // Account name already exists--report error
                return "The specified account name does not exist";
            }
        }

        public string ChangeAccountPassword(string accountName, string password, string confirmedPassword)
        {
            // Check that the passwords match
            if (string.IsNullOrEmpty(password) || password != confirmedPassword)
            {
                return "The passwords did not match.";
            }

            XDocument doc = XDocument.Load(XmlFile);
            try
            {
                // If one account isn't returned, the error is returned in the catch
                XElement account = (from a in doc.Descendants("account")
                                    where a.Attribute("name").Value == accountName
                                    select a).Single();

                // If no exception was thrown, change the password, save the data, and refresh the data source
                account.Attribute("password_hash").Value = password;
                doc.Save(XmlFile);
                RefreshXmlData();
                // No errors
                return "";
            }
            catch (Exception)
            {
                // Account name already exists--report error
                return "The specified account name does not exist";
            }
        }
        #endregion // Methods
    }
}
